前面介紹了互斥鎖可以避免資料競爭,但如果複數個執行緒都把持著某些資源、卻也都同時在等待其他執行緒手上的資源,就會形成「死結(deadlock)」。
這邊簡單帶過理論上形成死結四個應同時成立的「科夫曼(Coffman)條件」:
以下程式碼將形成死結:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mtxA;
std::mutex mtxB;
// 第一個執行緒:持有 A 鎖、等待 B 鎖
void threadOneWorker() {
std::cout << "Thread 1: Trying to lock A...\n";
std::unique_lock<std::mutex> lockA(mtxA); // 把 A 鎖拿走
std::cout << "Thread 1: Locked A successfully!\n";
// 小睡片刻,給第二個執行緒足夠的時間去拿 B 鎖
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// A 鎖想要去取 B 鎖
std::cout << "Thread 1: Trying to lock B...\n";
std::unique_lock<std::mutex> lockB(mtxB); // B 鎖已被第二個執行緒拿走,所以第一個執行緒永遠等不到
std::cout << "Thread 1: Locked B successfully!\n";
}
// 第一個執行緒:持有 B 鎖、等待 A 鎖
void threadTwoWorker() {
std::cout << "Thread 2: Trying to lock B...\n";
std::unique_lock<std::mutex> lockB(mtxB); // 把 B 鎖拿走
std::cout << "Thread 2: Locked B successfully!\n";
// 小睡片刻,給第一個執行緒足夠的時間去拿 A 鎖
std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::cout << "Thread 2: Trying to lock A...\n";
std::unique_lock<std::mutex> lockA(mtxA); // A 鎖已被第一個執行緒拿走,所以第二個執行緒永遠等不到
std::cout << "Thread 2: Locked A successfully!\n";
}
int main() {
std::thread t1(threadOneWorker);
std::thread t2(threadTwoWorker);
t1.join();
t2.join();
// 第一個執行緒 t1 持有 A 鎖、等待 B 鎖
// 第二個執行緒 t2 持有 B 鎖、等待 A 鎖
// 兩執行緒僵持不下,無法執行到這裡
std::cout << "Program finished smoothly.\n";
return 0;
}
在此範例中:
t1 抓到 A 鎖後,還需要 B 鎖,但 B 鎖被第二個執行緒 t2 抓著不放。t2 抓到 B 鎖後,還需要 A 鎖,但 A 鎖被第一個執行緒 t1 抓著不放。兩執行緒僵持不下,因此就卡住、形成「死結」,執行結果如下:
Thread 2: Trying to lock B...
Thread 2: Locked B successfully!
Thread 1: Trying to lock A...
Thread 1: Locked A successfully!
Thread 2: Trying to lock A...
Thread 1: Trying to lock B...
前四行兩執行緒都有順利拿到鎖,第五行開始則各自拿不到鎖,直接卡住,永遠無法抵達 Program finished smoothly。
既然形成死結要同時滿足四個科夫曼條件,那只要把其中一個條件拿掉,就可以解開死結。
四個條件中,最容易打破的是「循環等待」,讓每個執行緒都依照固定順序取得資源,避免兩執行緒各自取了某些資源、卻又都在等待對方手上的資源。
上例若改成「兩執行緒都先取 A 鎖再取 B 鎖」,便成解開死結,兩執行緒都能取得 A 鎖與 B 鎖:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mtxA;
std::mutex mtxB;
// 第一個執行緒不變,先拿 A 鎖再拿 B 鎖
void threadOneWorker() {
std::cout << "Thread 1: Trying to lock A...\n";
std::unique_lock<std::mutex> lockA(mtxA);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::cout << "Thread 1: Trying to lock B...\n";
std::unique_lock<std::mutex> lockB(mtxB);
std::cout << "Thread 1: Locked both successfully!\n";
}
// 第二個執行緒也改成先拿 A 鎖再拿 B 鎖
void threadTwoWorker() {
std::cout << "Thread 2: Trying to lock A...\n"; // 改先拿 A 鎖
std::unique_lock<std::mutex> lockA(mtxA);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::cout << "Thread 2: Trying to lock B...\n"; // 再拿 B 鎖
std::unique_lock<std::mutex> lockB(mtxB);
std::cout << "Thread 2: Locked both successfully!\n";
}
int main() {
std::thread t1(threadOneWorker);
std::thread t2(threadTwoWorker);
t1.join();
t2.join();
// 這行可成功顯示
std::cout << "Program finished smoothly.\n";
return 0;
}
執行結果如下:
Thread 1: Trying to lock A...
Thread 2: Trying to lock A...
Thread 1: Trying to lock B...
Thread 1: Locked both successfully!
Thread 2: Trying to lock B...
Thread 2: Locked both successfully!
Program finished smoothly.
說明如下:
std::scoped_lock這是 C++ 17 開始推出的防死結鎖,可以把兩個以上的互斥鎖統一管理,讓任一執行緒「持有全部資源」或「沒有全部資源」,而不會「拿了某些資源、卻在等待其他資源」,也就避免了死結的可能性。寫法如下:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mtxA;
std::mutex mtxB;
void threadOneWorker() {
std::cout << "Thread 1: Safely locking A and B via scoped_lock...\n";
// 第一個執行緒把兩個互斥鎖都放進 scoped_lock
std::scoped_lock lock(mtxA, mtxB);
std::cout << "Thread 1: Locked both successfully!\n";
std::this_thread::sleep_for(std::chrono::milliseconds(50));
} // 離開大括號,scoped_lock 自動把 mtxA 和 mtxB 兩個鎖釋放
void threadTwoWorker() {
std::cout << "Thread 2: Safely locking B and A via scoped_lock...\n";
// 第二個執行緒也把兩個互斥鎖都放進 scoped_lock
std::scoped_lock lock(mtxB, mtxA);
std::cout << "Thread 2: Locked both successfully!\n";
std::this_thread::sleep_for(std::chrono::milliseconds(50));
} // 離開大括號,scoped_lock 自動把 mtxB 和 mtxA 兩個鎖釋放
int main() {
std::thread t1(threadOneWorker);
std::thread t2(threadTwoWorker);
t1.join();
t2.join();
std::cout << "Program finished smoothly.\n";
return 0;
}
執行結果如下,可見兩個執行緒沒有互相卡住對方,有辦法執行到最後的 Program finished smoothly:
Thread 1: Safely locking A and B via scoped_lock...
Thread 1: Locked both successfully!
Thread 2: Safely locking B and A via scoped_lock...
Thread 2: Locked both successfully!
Program finished smoothly.
std::lock在 C++ 17 以前若要避免死結,則要先上 std::defer_lock 延遲上鎖後,再都丟進 std::lock 如下:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mtxA;
std::mutex mtxB;
void threadOneWorker() {
std::cout << "Thread 1: Safely locking A and B via std::lock...\n";
// 先放進 defer_lock 延遲上鎖
std::unique_lock<std::mutex> lockA(mtxA, std::defer_lock);
std::unique_lock<std::mutex> lockB(mtxB, std::defer_lock);
// 把兩個鎖同時丟給 std::lock,由演算法保證不發生死結
std::lock(lockA, lockB);
std::cout << "Thread 1: Locked both successfully!\n";
std::this_thread::sleep_for(std::chrono::milliseconds(50));
} // 離開大括號,lockA 和 lockB 自動解鎖
void threadTwoWorker() {
std::cout << "Thread 2: Safely locking B and A via std::lock...\n";
// 一樣先放進 defer_lock 延遲上鎖
std::unique_lock<std::mutex> lockB(mtxB, std::defer_lock);
std::unique_lock<std::mutex> lockA(mtxA, std::defer_lock);
// 把兩個鎖同時丟給 std::lock,由演算法保證不發生死結
std::lock(lockB, lockA);
std::cout << "Thread 2: Locked both successfully!\n";
std::this_thread::sleep_for(std::chrono::milliseconds(50));
} // 離開大括號,lockB 和 lockA 自動解鎖
int main() {
std::thread t1(threadOneWorker);
std::thread t2(threadTwoWorker);
t1.join(); t2.join();
std::cout << "Program finished smoothly.\n";
return 0;
}
執行結果如下:
Thread 1: Safely locking A and B via std::lock...
Thread 2: Safely locking B and A via std::lock...
Thread 1: Locked both successfully!
Thread 2: Locked both successfully!
Program finished smoothly.